#!/usr/bin/python -S
"""
p4_test.py: Tests for p4 JSON Patterns
"""

__author__ = 'Andy Chu'


import os
import sys

from pan.core import cmdapp
from pan.core import params
from pan.core import json
from pan.core import util
from pan.test import testy

if __name__ == '__main__':
  # for jsonpattern package
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python'))
  # Root of the project
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from tests import test_main

import jsonpattern


class P4Test(testy.Test):

  LABELS = ['multilanguage']

  def testP4Files(self):
    s = test_main.TestData('p4-files.txt')

    i = jsonpattern.Pattern(r'P| { \d+ | int}')
    word = jsonpattern.Pattern(r'P| { \w+ }')

    p = r"""
    |    repeated files
  SP|       { //[^#]+ depot-path }#{:int rev} - {:word action} {#}
  SP|       change {:int change} \({ [\w\+]+ file-type }\)\n
    |    end
"""
    p = testy.ClassDef(p, patterns={
        'int': i,
        'word': word,
        })

    self.verify.ParseTree(p)

    self.verify.Match(p, s, json.loads(test_main.TestData('p4-files.json')))

  def testFullChangeDesc_NewSyntax(self):
    s = test_main.TestData("p4-describe.txt")

    library = {}

    def Add(name, pattern, desc=None):
      library[name] = jsonpattern.Pattern(
          pattern, patterns=library, name=name, desc=desc)

    Add('int', r'P|   { \d+ | int }',
        desc="""
        The trusty <b>int subpattern</b> -- which uses the <b>int
        filter</b>.""")

    self.verify.Match(library['int'], '43', 43)

    Add('Y/M/D', r'P|   {:int year} / {:int month} / {:int day}',
        desc="Captures a date in this format.")

    self.verify.Match(library['Y/M/D'], '2009/07/04',
        {'year': 2009, 'month': 7, 'day': 4})

    Add('H:M:S',
        r'P|    {:int hour} : {:int minute} : {:int second}',
        desc="Captures a time in this format.")

    self.verify.Match(library['H:M:S'], '09:55:59',
        {'hour': 9, 'minute': 55, 'second': 59})

    Add('comma-list',
        r"""
  |    repeated +
 P|       { [\w\-]+ } ,?
  |    end
  """, desc="""
  One or more word characters, separated by commas.
  Both the repeated block and its values are <b>anonymous</b> here.""")

    self.verify.Match(library['comma-list'], 'andy,bob,foo-eng',
        ['andy', 'bob', 'foo-eng'])

    p = r"""
  #     Line one
 SP|    Change {:int change-num} by { \w+ username }@{ \w+ client } on {#}
 SP|    {:Y/M/D date} {:H:M:S time}
   |    $optional pending | bool
 SL|        {#} *pending*
   |    end

  #     Now the change description
  P|    \n\n
  P|    { .* desc }  {# TODO: .* isn't right, need a special construct}
  P|    \n

  #     Metadata block
  P|    \s* R={:comma-list reviewers}
  P|    \s* CC={:comma-list cc}
  P|    \s* 
   |    value delta
 SL|        DELTA={:int total}  {# 2 spaces}
 SL|        ({:int added} added, {:int deleted} deleted, {:int changed} changed)
   |    end
  P|    \n
  P|    \s* OCL={:int ocl}
  P|    \s*

  #     List of affected files
  P|    Affected files ...\n  {# TODO: This could be a "}
  P|    \n
   |    repeated files
 SL|        ... {#}
 SP|        { [^#]+ filename }#{:int revision} { \w+ action }\n
   |    end
"""
    Add('p4-desc', p, desc="""
    A pattern for a full change description.
    <p>
    Notice that we use the comma-list subpattern twice, the Y-M-D, H:M:S
    subpatterns, and the int subpattern many times.
    <p>
    Features used: an <b>optional</b>
    block with <b>scalar</b> ($) capture, and the <b>bool</b> filter.
    <p>
    <b>Scalar</b> capture means that we want the <b>text</b> matched within the
    block, rather than the captured values.  This is like the difference between
    <b>group 0</b> and <b>group 1</b> in Python, and Java, and <b>&amp;</b> and
    <b>$1</b> in Perl.
    <p>
    The <b>bool</b> filter converts an arbitrary JSON value to <b>true</b> or
    <b>false</b>, according to Python's rules.
    """)

    self.verify.Match(
        library['p4-desc'], s,
        json.loads(test_main.TestData("p4-describe.json")))

  def testMiniTutorial(self):
    s = test_main.TestData("p4-describe.txt")

    library = {}

    def Add(name, pattern, desc=None):
      library[name] = jsonpattern.Pattern(
          pattern, patterns=library, name=name, desc=desc)

    Add('time1', r'P|   Time:[ ] { \d+ hour } : { \d+ minute }',
        desc="""
        <p>
        Every line has a <b>prefix character</b> before the vertical bar
        <b>|</b>, and text after.  The P prefix means that the text after the
        pipe consists of patterns.
        <p>
        The text within the metacharacters <b>{</b> and <b>}</b> are JSON
        Pattern <b>expressions</b>, which generally do <b>named capture</b>.
        The things outside are regular expressions that must be matched.
        They're processed by the underlying regex engine.  
        <p>
        With the P prefix, the whitespace between <b>{}</b> expressions and text
        is stripped.
        <p>
        Here we capture the hour and minute from a time string:
        """)

    self.verify.Match(
        library['time1'], 'Time: 3:45', {'hour': '3', 'minute': '45'})

    Add('time2', r'P|   Time:[ ] { \d+ hour | int } : { \d+ minute | int }',
        desc="""
        <p>
        Suppose we want the integer 45 instead of the string "45".  You can
        apply <b>filters</b> inside JSON Pattern expressions to do this
        conversion.
        </p>
        """)

    self.verify.Match(
        library['time2'], 'Time: 3:45', {'hour': 3, 'minute': 45})

    Add('int', r'P|   { \d+ | int }',
        desc="""
        <p>
        We're reusing the same pattern twice, so let's define this
        <b>subpattern</b>.  If we omit the name, then the capture is
        <b>anonymous</b>.
        """)

    self.verify.Match(
        library['int'], '45', 45)

    Add('time', r'P|   {:int hour} : {:int minute}',
        desc="""
        <p>
        This is how we <b>refer</b> to subpatterns.  We can build up complex
        patterns using this mechanism.
        """)

    self.verify.Match(
        library['time'], '3:45', {'hour': 3, 'minute': 45})

    p = r"""
  #     Skip the heading lines
  P|    .*\n
  P|    .*\n

  #     The structured data
   |    repeated routes
 SP|        \s*{:time depart}  {:time arrive} { \w+ name }\n
   |    end
    """
    Add('train', p,
        desc="""
        <p>
        The real power of <b>JSON Pattern</b> is that we can build up
        hierarchical structures.  Here we match a bus schedule with two columns
        of numbers.
        <p>
        Lines with no prefix have <b>JSON Pattern keywords</b> on them.  The
        <b>repeated</b> keyword starts a block that repeats until its body
        cannot be matched.  On each iteration, the captured JSON is <b>appended
        to an array</b>.
        <p>
        The S prefix is like P, but indicates that whitespace is
        <b>significant</b>, which makes certain patterns more readable.   (This
        doesn't apply the beginning and end of the line, so you can use
        indentation to structure your programs).
        """)

    s = """\
Depart Arrive Name
------ ------ -----
 10:02  10:13 Dinky
 10:14  10:25 Binky
 11:02  11:13 Dinky
 11:14  11:25 Binky
"""
    expected = {
        'routes': [
            {'arrive': {'hour': 10, 'minute': 13},
             'depart': {'hour': 10, 'minute': 2},
             'name': 'Dinky'},
            {'arrive': {'hour': 10, 'minute': 25},
             'depart': {'hour': 10, 'minute': 14},
             'name': 'Binky'},
            {'arrive': {'hour': 11, 'minute': 13},
             'depart': {'hour': 11, 'minute': 2},
             'name': 'Dinky'},
            {'arrive': {'hour': 11, 'minute': 25},
             'depart': {'hour': 11, 'minute': 14},
             'name': 'Binky'}
             ]}
    self.verify.Match(library['train'], s, expected)


if __name__ == '__main__':
  test_main.main(__import__(__name__), sys.argv[1:])
